<!-- 通过用户滑动或编程的方式切换显示元素 -->
<template>
    <div
        class="overflow-hidden"
        v-if="list.length > 0"
        ref="slideListRef"
        @touchstart="handleTouchStart"
        @touchmove="handleTouchMove"
        @touchend="handleTouchEnd"
        @touchcancel="handleTouchCancel"
        @mousewheel="handleMouseWheel"
    >
        <div
            :style="getItemStyle()"
            v-if="currentIndex > 0 && slidingDistance > 0"
        >
            <slot
                name="item"
                :item="previousItem"
                :switchProgress="slidingDistance"
                status="previous"
            ></slot>
        </div>
        <div :style="getItemStyle()">
            <slot
                name="item"
                :item="currentItem"
                :switchProgress="slidingDistance"
                status="current"
            ></slot>
        </div>
        <div
            :style="getItemStyle()"
            v-if="currentIndex < list.length - 1 && slidingDistance < 0"
        >
            <slot
                name="item"
                :item="nextItem"
                :switchProgress="slidingDistance"
                status="next"
            ></slot>
        </div>
    </div>
</template>

<script setup lang="ts" generic="T">
    import { useResizeObserver } from "@vueuse/core"
    import {
        StyleValue,
        computed,
        onMounted,
        onUnmounted,
        reactive,
        ref,
    } from "vue"

    defineSlots<{
        item(props: {
            item: T
            switchProgress: number
            status: "previous" | "current" | "next"
        }): any
    }>()

    const props = withDefaults(
        defineProps<{
            list: T[]
            startIndex?: number
            slidingSensitivity?: number
            animationTime?: number
        }>(),
        {
            startIndex: 0,
            slidingSensitivity: 0.12,
            animationTime: 150,
        }
    )

    if (props.list.length > 0 && props.startIndex >= props.list.length) {
        throw `起始索引越界: ${props.startIndex}`
    }
    if (props.startIndex < 0) {
        throw `起始索引不可为负数: ${props.startIndex}`
    }
    if (!(props.slidingSensitivity < 1)) {
        throw `slidingSensitivity >= 1,slidingSensitivity:${props.slidingSensitivity}`
    }

    const currentIndex = ref(props.startIndex)
    const previousItem = computed(() => props.list[currentIndex.value - 1])
    const currentItem = computed(() => props.list[currentIndex.value])
    const nextItem = computed(() => props.list[currentIndex.value + 1])

    const rect = reactive({
        width: 0,
        height: 0,
    })
    const slideListRef = ref()
    useResizeObserver(slideListRef, (entries) => {
        const r = entries[0].contentRect
        rect.width = r.width
        rect.height = r.height
    })

    const slidingDistance = ref(0)

    function getItemStyle(): StyleValue {
        let offset: number
        if (slidingDistance.value == 0) {
            offset = 0
        } else if (slidingDistance.value > 0) {
            if (currentIndex.value > 0) {
                offset = -rect.height + slidingDistance.value
            } else {
                offset = slidingDistance.value
            }
        } else {
            offset = slidingDistance.value
        }
        return {
            width: `${rect.width}px`,
            height: `${rect.height}px`,
            transform: `translate(0px,${offset}px)`,
        }
    }

    const touchId = ref()
    const previousTouchPoint = ref(0)
    const isSliding = ref(false)

    function handleTouchStart(event: TouchEvent) {
        if (!touchId.value) {
            touchId.value = event.touches[0].identifier
            previousTouchPoint.value = event.touches[0].pageY
            if (isSliding.value) {
                isSliding.value = false
            }
        }
    }
    function handleTouchMove(event: TouchEvent) {
        event.preventDefault()
        for (const touch of event.touches) {
            if (touch.identifier != touchId.value) return
            slidingDistance.value += touch.pageY - previousTouchPoint.value
            previousTouchPoint.value = touch.pageY
            if (
                (slidingDistance.value > 0 && currentIndex.value < 1) ||
                (slidingDistance.value < 0 &&
                    currentIndex.value >= props.list.length - 1)
            ) {
                let min = Math.min(
                    rect.height * 0.1,
                    rect.height * props.slidingSensitivity - 1
                )
                if (min < 1) min = 1
                if (Math.abs(slidingDistance.value) > min) {
                    // debugger
                    slidingDistance.value =
                        slidingDistance.value > 0 ? min : -min
                }
            }
            if (slidingDistance.value >= rect.height) {
                currentIndex.value += slidingDistance.value > 0 ? -1 : 1
                slidingDistance.value +=
                    slidingDistance.value > 0 ? -rect.height : rect.height
            }
        }
    }
    function handleTouchEnd(event: TouchEvent) {
        for (const touch of event.changedTouches) {
            if (touch.identifier != touchId.value) return
            touchId.value = undefined
            if (
                Math.abs(slidingDistance.value) / rect.height >=
                props.slidingSensitivity
            ) {
                if (Math.abs(slidingDistance.value) == rect.height) {
                    currentIndex.value += slidingDistance.value > 0 ? -1 : 1
                    slidingDistance.value = 0
                    return
                }

                currentIndex.value += slidingDistance.value > 0 ? -1 : 1
                slidingDistance.value +=
                    slidingDistance.value > 0 ? -rect.height : rect.height
                const to = 0
                slideTo(to)
            } else {
                if (slidingDistance.value != 0) {
                    slideTo(0)
                }
            }
        }
    }
    function handleTouchCancel(event: TouchEvent) {
        for (const touch of event.changedTouches) {
            if (touch.identifier != touchId.value) return
            touchId.value = undefined
            slideTo(0)
        }
    }
    function slideTo(to: number): Promise<boolean> {
        isSliding.value = true
        const startingPoint = slidingDistance.value
        const startTimestamp = performance.now()
        const totalDistance = Math.abs(to - startingPoint)
        let time =
            (Math.abs(to - slidingDistance.value) / rect.height) *
            props.animationTime
        if (time > props.animationTime) {
            time = props.animationTime
        } else if (time < 1) {
            time = 1
        }
        let promiseResolve: (r: boolean) => void
        const promise: Promise<boolean> = new Promise((resolve) => {
            promiseResolve = resolve
        })

        function slide(timestamp: number) {
            if (!isSliding.value) return
            let currentSlidingDistance =
                ((timestamp - startTimestamp) / time) * totalDistance
            if (currentSlidingDistance >= totalDistance) {
                currentSlidingDistance = totalDistance
                slidingDistance.value =
                    startingPoint +
                    (to > startingPoint
                        ? currentSlidingDistance
                        : -currentSlidingDistance)
                isSliding.value = false
                promiseResolve(true)
            } else {
                slidingDistance.value =
                    startingPoint +
                    (to > startingPoint
                        ? currentSlidingDistance
                        : -currentSlidingDistance)
                requestAnimationFrame(slide)
            }
        }

        requestAnimationFrame(slide)

        return promise
    }

    function setCurrentIndex(index: number) {
        if (index < 0 || index > props.list.length - 1) {
            throw `索引越界，index: ${index}, list.length: ${props.list.length}`
        }
        currentIndex.value = index
        slidingDistance.value = 0
    }

    const switching = ref(false)
    function switchToIndex(index: number) {
        if (index < 0 || index > props.list.length - 1) {
            throw `索引越界，index: ${index}, list.length: ${props.list.length}`
        }

        let promiseResolve: (r: boolean) => void
        const promise = new Promise((resolve) => (promiseResolve = resolve))
        switching.value = true

        function _switchToIndex(index: number) {
            if (currentIndex.value == index) {
                switching.value = false
                promiseResolve(true)
            } else {
                const difference = index - currentIndex.value
                currentIndex.value += difference > 0 ? 1 : -1
                slidingDistance.value =
                    difference > 0 ? rect.height : -rect.height
                slideTo(0).then((r) => {
                    if (r) {
                        _switchToIndex(index)
                    } else {
                        // 出错
                        switching.value = false
                        promiseResolve(false)
                    }
                })
            }
        }

        _switchToIndex(index)
        return promise
    }

    function switchPrevious() {
        if (currentIndex.value < 1) {
            throw "not find pervious"
        }
        return switchToIndex(currentIndex.value - 1)
    }

    function switchNext() {
        if (currentIndex.value >= props.list.length) {
            throw "not find next"
        }
        return switchToIndex(currentIndex.value + 1)
    }

    function handleMouseWheel(e: any) {
        if (switching.value) return
        if (e.wheelDelta > 0) {
            if (currentIndex.value > 0) {
                switchPrevious()
            }
        } else {
            if (currentIndex.value < props.list.length - 1) {
                switchNext()
            }
        }
    }

    onMounted(() => {
        document.addEventListener("keyup", handleKeyup)
    })

    onUnmounted(() => {
        document.removeEventListener("keyup", handleKeyup)
    })

    function handleKeyup(e: KeyboardEvent) {
        if (switching.value) return
        if (e.key == "ArrowUp") {
            if (currentIndex.value > 0) {
                switchPrevious()
            }
        } else if (e.key == "ArrowDown") {
            if (currentIndex.value < props.list.length - 1) {
                switchNext()
            }
        }
    }

    defineExpose({
        setCurrentIndex,
        switchToIndex,
        switchPrevious,
        switchNext,
    })
</script>
